Ordenamiento de listas

Las listas se pueden ordenar fácilmente usando la función sorted:


In [2]:
lista_de_numeros = [1, 6, 3, 9, 5, 2]
lista_ordenada = sorted(lista_de_numeros)
print lista_ordenada
print lista_de_numeros


[1, 2, 3, 5, 6, 9]
[1, 6, 3, 9, 5, 2]

Pero, ¿y cómo hacemos para ordenarla de mayor a menor?.
Simple, interrogamos un poco a la función:

>>> print sorted.__doc__
sorted(iterable, cmp=None, key=None, reverse=False) --> new sorted list

Entonces, con sólo pasarle el parámetro de reverse en True debería alcanzar:


In [3]:
lista_de_numeros = [1, 6, 3, 9, 5, 2]
print sorted(lista_de_numeros, reverse=True)


[9, 6, 5, 3, 2, 1]

¿Y si lo que quiero ordenar es una lista de registros?.
Podemos pasarle una función que sepa cómo comparar esos registros o una que sepa devolver la información que necesita comparar.


In [4]:
def crear_curso():
    curso = [
        {'nombre': 'Rodriguez, Carlos', 'nota': 6, 'padron': 98128}, 
        {'nombre': 'Perez, Lucas', 'nota': 6, 'padron': 93453}, 
        {'nombre': 'Gonzalez, Ramiro', 'nota': 8, 'padron': 93716}, 
        {'nombre': 'Gonzalez, Carlos', 'nota': 6, 'padron': 90464}, 
        {'nombre': 'Lopez, Carlos', 'nota': 7, 'padron': 98569}
    ]
    
    return curso


def imprimir_curso(lista):
    for idx, x in enumerate(lista):
        msg = '    {pos:2}. {padron} - {nombre}: {nota}'
        print msg.format(pos=idx, **x)


def obtener_padron(alumno):
    return alumno['padron']


curso = crear_curso()
print 'La lista tiene los alumnos:'
imprimir_curso(curso)

lista_ordenada = sorted(curso, key=obtener_padron)
print 'Y la lista ordenada por padrón:'
imprimir_curso(lista_ordenada)


La lista tiene los alumnos:
     0. 98128 - Rodriguez, Carlos: 6
     1. 93453 - Perez, Lucas: 6
     2. 93716 - Gonzalez, Ramiro: 8
     3. 90464 - Gonzalez, Carlos: 6
     4. 98569 - Lopez, Carlos: 7
Y la lista ordenada por padrón:
     0. 90464 - Gonzalez, Carlos: 6
     1. 93453 - Perez, Lucas: 6
     2. 93716 - Gonzalez, Ramiro: 8
     3. 98128 - Rodriguez, Carlos: 6
     4. 98569 - Lopez, Carlos: 7

Búsquedas en listas

Para saber si un elemento se encuentra en una lista, alcanza con usar el operador in:


In [5]:
lista = [11, 4, 6, 1, 3, 5, 7]

if 3 in lista:
    print '3 esta en la lista'
else:
    print '3 no esta en la lista'

if 15 in lista:
    print '15 esta en la lista'
else:
    print '15 no esta en la lista'


3 esta en la lista
15 no esta en la lista

También es muy fácil saber si un elemento no esta en la lista:


In [6]:
lista = [11, 4, 6, 1, 3, 5, 7]

if 3 not in lista:
    print '3 NO esta en la lista'
else:
    print '3 SI esta en la lista'


3 SI esta en la lista

En cambio, si lo que queremos es saber es dónde se encuentra el número 3 en la lista es:


In [7]:
lista = [11, 4, 6, 1, 3, 5, 7]

pos = lista.index(3)
print 'El 3 se encuentra en la posición', pos

pos = lista.index(15)
print 'El 15 se encuentra en la posición', pos


El 3 se encuentra en la posición 4
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-7-d0e7e1660268> in <module>()
      4 print 'El 3 se encuentra en la posición', pos
      5 
----> 6 pos = lista.index(15)
      7 print 'El 15 se encuentra en la posición', pos

ValueError: 15 is not in list

Ahora, para todos estos casos lo que hice fue buscar un elemento completo, es decir, que tenía que conocer todo lo que buscaba y no sólamente una parte, como podría ser el padrón de un alumno.


In [10]:
curso = crear_curso()
print 'La lista tiene los alumnos:'
imprimir_curso(curso)

alumno_93716 = (alumno for alumno in curso if alumno['padron'] == 93716).next()
print 'El alumno de padron 93716 se llama {nombre}'.format(**alumno_93716)


La lista tiene los alumnos:
     0. 98128 - Rodriguez, Carlos: 6
     1. 93453 - Perez, Lucas: 6
     2. 93716 - Gonzalez, Ramiro: 8
     3. 90464 - Gonzalez, Carlos: 6
     4. 98569 - Lopez, Carlos: 7
El alumno de padron 93716 se llama Gonzalez, Ramiro

Funciones anónimas

Hasta ahora, a todas las funciones que creamos les poníamos un nombre al momento de crearlas, pero cuando tenemos que crear funciones que sólo tienen una línea y no se usan en una gran cantidad de lugares se pueden usar las funciones lambda:


In [11]:
help("lambda")


Lambdas
*******

   lambda_expr     ::= "lambda" [parameter_list]: expression
   old_lambda_expr ::= "lambda" [parameter_list]: old_expression

Lambda expressions (sometimes called lambda forms) have the same
syntactic position as expressions.  They are a shorthand to create
anonymous functions; the expression "lambda arguments: expression"
yields a function object.  The unnamed object behaves like a function
object defined with

   def name(arguments):
       return expression

See section Function definitions for the syntax of parameter lists.
Note that functions created with lambda expressions cannot contain
statements.

Related help topics: FUNCTIONS


In [12]:
mi_funcion = lambda x, y: x+y

resultado = mi_funcion(1, 2)
print resultado
print type(mi_funcion)


3
<type 'function'>

In [13]:
def mi_funcion2(x, y):
    return x + y

resultado = mi_funcion2(1, 2)
print resultado
print type(mi_funcion2)


3
<type 'function'>

Si bien no son funciones que se usen todos los días, se suelen usar cuando una función recibe otra función como parámetro (las funciones son un tipo de dato, por lo que se las pueden asignar a variables, y por lo tanto, también pueden ser parámetros). Por ejemplo, para ordenar los alumnos por padrón podríamos usar:

sorted(curso, key=lambda x: x['padron'])

Ahora, si quiero ordenar la lista anterior por nota decreciente y, en caso de igualdad, por padrón podríamos usar:


In [14]:
curso = crear_curso()
print 'Curso original'
imprimir_curso(curso)

lista_ordenada = sorted(curso, key=lambda alumno: (-alumno['nota'], alumno['padron']))
print 'Curso ordenado'
imprimir_curso(lista_ordenada)


Curso original
     0. 98128 - Rodriguez, Carlos: 6
     1. 93453 - Perez, Lucas: 6
     2. 93716 - Gonzalez, Ramiro: 8
     3. 90464 - Gonzalez, Carlos: 6
     4. 98569 - Lopez, Carlos: 7
Curso ordenado
     0. 93716 - Gonzalez, Ramiro: 8
     1. 98569 - Lopez, Carlos: 7
     2. 90464 - Gonzalez, Carlos: 6
     3. 93453 - Perez, Lucas: 6
     4. 98128 - Rodriguez, Carlos: 6

Excepciones

Una excepción es la forma que tiene el intérprete de que indicarle al programador y/o usuario que ha ocurrido un error. Si la excepción no es controlada por el desarrollador ésta llega hasta el usuario y termina abruptamente la ejecución del sistema.
Por ejemplo:


In [15]:
print 1/0


---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-15-e19d6e6ac7e1> in <module>()
----> 1 print 1/0

ZeroDivisionError: integer division or modulo by zero

Pero no hay que tenerle miedo a las excepciones, sólo hay que tenerlas en cuenta y controlarlas en el caso de que ocurran:


In [16]:
dividendo = 10
divisor = '0'
print 'Intentare hacer la división de {}/{}'.format(dividendo, divisor)
try:
    resultado = dividendo / divisor
    print resultado
except ZeroDivisionError:
    print 'No se puede hacer la división ya que el divisor es 0.'
except TypeError:
    print 'Alguno de los parametros no es un número'

print 'Algo'


Intentare hacer la división de 10/0
Alguno de los parametros no es un número
Algo

Pero supongamos que implementamos la regla de tres de la siguiente forma:


In [17]:
def dividir(x, y):
    return x/y


def regla_de_tres(x, y, z):
    return dividir(z*y, x)


# Si de 28 alumnos, aprobaron 15, el porcentaje de aprobados es de...
porcentaje_de_aprobados = regla_de_tres(28, 15, 100)
print 'Porcentaje de aprobados: {0:.2f}%'.format(porcentaje_de_aprobados)


Porcentaje de aprobados: 53.00%

En cambio, si le pasamos 0 en el lugar de x:


In [18]:
resultado = regla_de_tres(0, 13, 100)
print 'Porcentaje de aprobados: {0:.2f}%'.format(resultado)


---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-18-23c25e27495d> in <module>()
----> 1 resultado = regla_de_tres(0, 13, 100)
      2 print 'Porcentaje de aprobados: {0:.2f}%'.format(resultado)

<ipython-input-17-eea01b07ba66> in regla_de_tres(x, y, z)
      4 
      5 def regla_de_tres(x, y, z):
----> 6     return dividir(z*y, x)
      7 
      8 

<ipython-input-17-eea01b07ba66> in dividir(x, y)
      1 def dividir(x, y):
----> 2     return x/y
      3 
      4 
      5 def regla_de_tres(x, y, z):

ZeroDivisionError: integer division or modulo by zero

Acá podemos ver todo el traceback o stacktrace, que son el cómo se fueron llamando las distintas funciones entre sí hasta que llegamos al error.
Pero no es bueno que este tipo de excepciones las vea directamente el usuario, por lo que podemos controlarlas en distintos momentos. Se pueden controlar inmediatamente donde ocurre el error, como mostramos antes, o en cualquier parte de este stacktrace.
En el caso de la regla_de_tres no nos conviene poner el try/except encerrando la línea x/y, ya que en ese punto no tenemos toda la información que necesitamos para informarle correctamente al usuario, por lo que podemos ponerla en:


In [19]:
def dividir(x, y):
    return x/y


def regla_de_tres(x, y, z):
    resultado = 0
    try:
        resultado = dividir(z*y, x)
    except ZeroDivisionError:
        print 'No se puede calcular la regla de tres ' \
              'porque el divisor es 0'
        
    return resultado
    
    
print regla_de_tres(0, 1, 2)


No se puede calcular la regla de tres porque el divisor es 0
0

Pero en este caso igual muestra 0, por lo que si queremos, podemos poner los try/except incluso más arriba en el stacktrace:


In [20]:
def dividir(x, y):
    return x/y


def regla_de_tres(x, y, z):
    return dividir(z*y, x)
    
    
try:
    print regla_de_tres(0, 1, 2)
except ZeroDivisionError:
    print 'No se puede calcular la regla de tres ' \
          'porque el divisor es 0'


No se puede calcular la regla de tres porque el divisor es 0

Todos los casos son distintos y no hay UN lugar ideal dónde capturar la excepción; es cuestión del desarrollador decidir dónde conviene ponerlo para cada problema.

Capturar múltiples excepciones

Una única línea puede lanzar distintas excepciones, por lo que capturar un tipo de excepción en particular no me asegura que el programa no pueda lanzar un error en esa línea que supuestamente es segura: En algunos casos tenemos en cuenta que el código puede lanzar una excepción como la de ZeroDivisionError, pero eso puede no ser suficiente:


In [21]:
def dividir_numeros(x, y):
    try:
        resultado = x/y
        print 'El resultado es: %s' % resultado
    except ZeroDivisionError:
        print 'ERROR: Ha ocurrido un error por dividir por 0'

        
dividir_numeros(1, 0)
dividir_numeros(10, 2)
dividir_numeros("10", 2)


ERROR: Ha ocurrido un error por dividir por 0
El resultado es: 5
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-21-90299ef64f0d> in <module>()
      9 dividir_numeros(1, 0)
     10 dividir_numeros(10, 2)
---> 11 dividir_numeros("10", 2)

<ipython-input-21-90299ef64f0d> in dividir_numeros(x, y)
      1 def dividir_numeros(x, y):
      2     try:
----> 3         resultado = x/y
      4         print 'El resultado es: %s' % resultado
      5     except ZeroDivisionError:

TypeError: unsupported operand type(s) for /: 'str' and 'int'

En esos casos podemos capturar más de una excepción de la siguiente forma:


In [22]:
def dividir_numeros(x, y):
    try:
        resultado = x/y
        print 'El resultado es: %s' % resultado
    except TypeError:
        print 'ERROR: Ha ocurrido un error por mezclar tipos de datos'
    except ZeroDivisionError:
        print 'ERROR: Ha ocurrido un error de división por cero'
    except Exception:
        print 'ERROR: Ha ocurrido un error inesperado'

        
dividir_numeros(1, 0)
dividir_numeros(10, 2)
dividir_numeros("10", 2)


ERROR: Ha ocurrido un error de división por cero
El resultado es: 5
ERROR: Ha ocurrido un error por mezclar tipos de datos

Incluso, si queremos que los dos errores muestren el mismo mensaje podemos capturar ambas excepciones juntas:


In [23]:
def dividir_numeros(x, y):
    try:
        resultado = x/y
        print 'El resultado es: %s' % resultado
    except (ZeroDivisionError, TypeError):
        print 'ERROR: No se puede calcular la división'

        
dividir_numeros(1, 0)
dividir_numeros(10, 2)
dividir_numeros("10", 2)


ERROR: No se puede calcular la división
El resultado es: 5
ERROR: No se puede calcular la división

Jerarquía de excepciones

Existe una jerarquía de excepciones, de forma que si se sabe que puede venir un tipo de error, pero no se sabe exactamente qué excepción puede ocurrir siempre se puede poner una excepción de mayor jerarquía:

Por lo que el error de división por cero se puede evitar como:


In [24]:
try:
    print 1/0
except ZeroDivisionError:
    print 'Ha ocurrido un error de división por cero'


Ha ocurrido un error de división por cero

Y también como:


In [25]:
try:
    print 1/0
except Exception:
    print 'Ha ocurrido un error inesperado'


Ha ocurrido un error inesperado

Si bien siempre se puede poner Exception en lugar del tipo de excepción que se espera, no es una buena práctica de programación ya que se pueden esconder errores indeseados. Por ejemplo, un error de sintaxis. Además, cuando se lanza una excepción en el bloque try, el intérprete comienza a buscar entre todas cláusulas except una que coincida con el error que se produjo, o que sea de mayor jerarquía. Por lo tanto, es recomendable poner siempre las excepciones más específicas al principio y las más generales al final:

def dividir_numeros(x, y):
    try:
        resultado = x/y
        print 'El resultado es: %s' % resultado
    except TypeError:
        print 'ERROR: Ha ocurrido un error por mezclar tipos de datos'
    except ZeroDivisionError:
        print 'ERROR: Ha ocurrido un error de división por cero'
    except Exception:
        print 'ERROR: Ha ocurrido un error inesperado'

Si el error no es capturado por ninguna clausula se propaga de la misma forma que si no se hubiera puesto nada.

Otras cláusulas para el manejo de excepciones

Además de las cláusulas try y except existen otras relacionadas con las excepciones que nos permiten manejar de mejor manera el flujo del programa:

  • else: se usa para definir un bloque de código que se ejecutará sólo si no ocurrió ningún error.
  • finally: se usa para definir un bloque de código que se ejecutará siempre, independientemente de si se lanzó una excepción o no.

In [26]:
def dividir_numeros(x, y):
    try:
        resultado = x/y
        print 'El resultado es {}'.format(resultado)
    except ZeroDivisionError:
        print 'Error: División por cero'
    else:
        print 'Este mensaje se mostrará sólo si no ocurre ningún error'
    finally: 
        print 'Este bloque de código se muestra siempre'

        
dividir_numeros(1, 0)
print '-------------'
dividir_numeros(10, 2)


Error: División por cero
Este bloque de código se muestra siempre
-------------
El resultado es 5
Este mensaje se mostrará sólo si no ocurre ningún error
Este bloque de código se muestra siempre

Pero entonces, ¿por qué no poner ese código dentro del try-except?. Porque tal vez no queremos capturar con las cláusulas except lo que se ejecute en ese bloque de código:


In [27]:
def dividir_numeros(x, y):
    try:
        resultado = x/y
        print 'El resultado es {}'.format(resultado)
    except ZeroDivisionError:
        print 'Error: División por cero'
    else:
        print 'Ahora hago que ocurra una excepción'
        print 1/0
    finally: 
        print 'Este bloque de código se muestra siempre'

        
dividir_numeros(1, 0)
print '-------------'
dividir_numeros(10, 2)


Error: División por cero
Este bloque de código se muestra siempre
-------------
El resultado es 5
Ahora hago que ocurra una excepción
Este bloque de código se muestra siempre
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-27-9b1f34f6b1a2> in <module>()
     14 dividir_numeros(1, 0)
     15 print '-------------'
---> 16 dividir_numeros(10, 2)

<ipython-input-27-9b1f34f6b1a2> in dividir_numeros(x, y)
      7     else:
      8         print 'Ahora hago que ocurra una excepción'
----> 9         print 1/0
     10     finally:
     11         print 'Este bloque de código se muestra siempre'

ZeroDivisionError: integer division or modulo by zero

Lanzar excepciones

Hasta ahora vimos cómo capturar un error y trabajar con él sin que el programa termine abruptamente, pero en algunos casos somos nosotros mismos quienes van a querer lanzar una excepción. Y para eso, usaremos la palabra reservada raise:


In [28]:
def dividir_numeros(x, y):
    if y == 0:
        raise Exception('Error de división por cero')
    
    resultado = x/y
    print 'El resultado es {0}'.format(resultado)

    
try:
    dividir_numeros(1, 0)
except ZeroDivisionError as e:
    print 'ERROR: División por cero'
except Exception as e:
    print 'ERROR: ha ocurrido un error del tipo Exception'

print '----------'
dividir_numeros(1, 0)


ERROR: ha ocurrido un error del tipo Exception
----------
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-28-bf43f25320e8> in <module>()
     15 
     16 print '----------'
---> 17 dividir_numeros(1, 0)

<ipython-input-28-bf43f25320e8> in dividir_numeros(x, y)
      1 def dividir_numeros(x, y):
      2     if y == 0:
----> 3         raise Exception('Error de división por cero')
      4 
      5     resultado = x/y

Exception: Error de división por cero

Para más información, ingresar a https://docs.python.org/2/tutorial/errors.html

Ejercicios

  1. Se leen dos listas A y B, de N y M elementos respectivamente. Construir un algoritmo que retorne las listas unión e intersección de A y B. Previamente habrá que ordenarlos.
  2. Escribir una función que reciba una lista desordenada y un elemento, que:
    1. Busque todos los elementos coincidan con el pasado por parámetro y devuelva la cantidad de coincidencias encontradas.
    2. Busque la primera coincidencia del elemento en la lista y devuelva su posición.
      Si tienen la lista [1, 2, 7, 2, 32, 2, 9] y le aplican la función buscando el número 2 les tiene que devolver que aparece 3 veces y la primera vez es en la posición 1.
  3. Escribir una función que reciba una lista de números no ordenada, que:
    1. Devuelva el valor máximo.
    2. Devuelva una tupla que incluya el valor máximo y su posición.
    3. ¿Qué sucede si los elementos son cadenas de caracteres?
      Nota: no utilizar lista.sort() ni la función sorted.
  4. Se cuenta con una lista ordenada de productos, en la que uno consiste en una tupla de (identificador, descripción, precio), y una lista de los productos a facturar, en la que cada uno consiste en una tupla de (identificador, cantidad).
    Se desea generar una factura que incluya la cantidad, la descripción, el precio unitario y el precio total de cada producto comprado, y al final imprima el total general.
    Escribir una función que reciba ambas listas e imprima por pantalla la factura solicitada.
  5. Leer de teclado (usando la función raw_input) los datos de un listado de alumnos terminados con padrón 0. Para cada alumno deben leer:
    # Padrón
    # Nombre
    # Apellido
    # Nota del primer parcial
    # Nota del primer recuperatorio (en caso de no haber aprobado el parcial)
    # Nota del segundo recuperatorio (en caso de no haber aprobado en el primero)
    # Nombre del grupo
    # Nota del TP 1
    # Nota del TP 2
    Si el padrón es 0, no deben seguir pidiendo el resto de los campos.
    Tanto el padrón, como el nombre y apellido deben leerse como strings (existen padrones que comienzan con una letra b), pero debe validarse que se haya ingresado algo de por lo menos 2 caracteres.
    Todas las notas serán números enteros entre 0 y 10, aunque puede ser que el usuario accidentalmente ingrese algo que no sea un número, por lo que deberán validar la entrada y volver a pedirle los datos al usuario hasta que ingrese algo válido. También deben validar que las notas pertenezcan al rango de 0 a 10.
    Se asume que todos los alumnos se presentan a todos los parciales hasta aprobar o completar sus 3 chances.
    Al terminar deben:

    1. imprimir por pantalla un listado de todos los alumnos en condiciones de rendir coloquio (último parcial aprobado y todos los TP aprobados) en el mismo orden en el que el usuario los ingreso.
    2. imprimir por pantalla un listado de todos los alumnos en condiciones de rendir coloquio (último parcial aprobado y todos los TP aprobados) ordenados por padrón en forma creciente.
    3. imprimir por pantalla un listado de todos los alumnos en condiciones de rendir coloquio (último parcial aprobado y todos los TP aprobados) ordenados por nota y, en caso de igualdad, por padrón (ambos en forma creciente).
    4. Calcular para cada alumno el promedio de sus notas del parcial y luego el promedio del curso como el promedio de todos los promedios.
    5. Informar cuál es la nota que más se repite entre todos los parciales (sin importar si es primer, segundo o tercer parcial) e indicar la cantidad de ocurrencias.
    6. listar todas las notas que se sacaron los alumnos en el primer parcial y los padrones de quienes se sacaron esas notas con el siguiente formato:
    Nota: 2
    * nnnn1
    * nnnn2
    * nnnn3
    * nnnn4
    Nota: 4
    * nnnn1
    * nnnn2
    ...

    Tener en cuenta que las notas pueden ser del 2 al 10 y puede ocurrir que nadie se haya sacado esa nota (y en dicho caso no esa nota no tiene que aparecer en el listado)